@vantran-se/openchamber-web 2.1.1 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (442) hide show
  1. package/README.md +32 -24
  2. package/bin/cli.js +1 -3
  3. package/bin/cli.test.js +40 -16
  4. package/bin/lib/DOCUMENTATION.md +6 -5
  5. package/bin/lib/cli-args.js +6 -1
  6. package/bin/lib/cli-args.test.js +34 -0
  7. package/bin/lib/cli-lifecycle.js +0 -1
  8. package/bin/lib/cli-network.js +8 -2
  9. package/bin/lib/cli-process.js +1 -1
  10. package/bin/lib/cli-settings-accessors.test.js +21 -13
  11. package/bin/lib/cli-startup.js +17 -11
  12. package/bin/lib/cli-startup.test.js +92 -2
  13. package/bin/lib/commands-connect-url.js +8 -1
  14. package/bin/lib/commands-schedule.js +8 -2
  15. package/bin/lib/commands-serve.js +15 -21
  16. package/bin/lib/commands-update.js +12 -24
  17. package/bin/lib/commands-update.test.js +14 -46
  18. package/dist/assets/DiffView-Cv9MSXl1.js +4 -0
  19. package/dist/assets/FilePreviewCommentMenu-D4kLseHj.js +4 -0
  20. package/dist/assets/FilesView-BVoD8I2x.js +14 -0
  21. package/dist/assets/{GitView-DzuzhIPx.js → GitView-Bzb8p-ew.js} +4 -4
  22. package/dist/assets/GuestSurfacePane-Crdbvsxo.js +1 -0
  23. package/dist/assets/InProgressOperationBanner-CwMjNQgk.js +2 -0
  24. package/dist/assets/{JsonTreeView-B_EPRjzy.js → JsonTreeView-DRKHZfsn.js} +1 -1
  25. package/dist/assets/MarkdownImageGallery-DZtqT1Zn.js +1 -0
  26. package/dist/assets/MarkdownRendererImpl-CqG7HCNd.js +10 -0
  27. package/dist/assets/OnboardingScreen-DMIxwYK5.js +1 -0
  28. package/dist/assets/{PatchDiff-6yh1csza.js → PatchDiff-CUcCg1jk.js} +1 -1
  29. package/dist/assets/PierreDiffViewer-CnnR-dnD.js +228 -0
  30. package/dist/assets/PlanView-RCBadcY7.js +5 -0
  31. package/dist/assets/SettingsView-CjBjZFRO.js +30 -0
  32. package/dist/assets/SettingsWindow-DyM7QuhI.js +1 -0
  33. package/dist/assets/SubmoduleDiffSummary-BWIfgMED.js +1 -0
  34. package/dist/assets/ToolOutputDialog-Q0P3FuAi.js +15 -0
  35. package/dist/assets/ToolPartDiffPreview-p7qR9FpU.js +8 -0
  36. package/dist/assets/{VirtualizedFileDiff-BTJVRkyo.js → VirtualizedFileDiff-CFpjL8Ns.js} +3 -3
  37. package/dist/assets/{Virtualizer-xCEYz34p.js → Virtualizer-C0IsclZn.js} +1 -1
  38. package/dist/assets/WalkthroughView-DI31zD02.js +4 -0
  39. package/dist/assets/appThemeRegistry-B9gkFq9i.js +1 -0
  40. package/dist/assets/{computeVirtualFileMetrics-CmPe29DM.js → computeVirtualFileMetrics-C2WM6JC2.js} +2 -2
  41. package/dist/assets/de-YKfk5MmC.js +14 -0
  42. package/dist/assets/es-_gwT0kr-.js +20 -0
  43. package/dist/assets/fileTreeStatus-D438yi8M.js +1 -0
  44. package/dist/assets/fr-DqzvXmIm.js +18 -0
  45. package/dist/assets/index-BLaHKdtx.js +1 -0
  46. package/dist/assets/index-BkMXRrVQ.css +1 -0
  47. package/dist/assets/index-DxNa7xYB.js +65 -0
  48. package/dist/assets/initializeSharedPreferences-Bck5EAWP.js +1 -0
  49. package/dist/assets/{isDiffPlainText-CQmDCnEv.js → isDiffPlainText-Dgbw8VKk.js} +1 -1
  50. package/dist/assets/{isFilePlainText-CYBUzW4I.js → isFilePlainText-_ZS5UYRj.js} +1 -1
  51. package/dist/assets/ja-B-GFYhQE.js +20 -0
  52. package/dist/assets/ko-mzksfqDC.js +20 -0
  53. package/dist/assets/main-C5Xx8dn_.js +2 -0
  54. package/dist/assets/main-CfBSQM1H.js +990 -0
  55. package/dist/assets/markdownCore-BmWwW1ll.js +7 -0
  56. package/dist/assets/miniChat-mnKwy4vQ.js +2 -0
  57. package/dist/assets/mobile-DeEQd0wZ.js +2 -0
  58. package/dist/assets/nl-CWQr6NSq.js +20 -0
  59. package/dist/assets/{parseDiffFromFile-Cy0PNxun.js → parseDiffFromFile-BUgn-T9p.js} +2 -2
  60. package/dist/assets/patchFileDiff-Dt8LhgBh.js +13 -0
  61. package/dist/assets/pl-UmRcDUX2.js +20 -0
  62. package/dist/assets/pt-BR-BSLybvVF.js +20 -0
  63. package/dist/assets/relocateRestoredSession-B5E9xb3T.js +1 -0
  64. package/dist/assets/renderElectronMiniChatApp-C0a9A5_J.js +2 -0
  65. package/dist/assets/renderMobileApp-BNncg3Pk.js +25 -0
  66. package/dist/assets/runtimeEndpointReset-CZ5aafpl.js +62 -0
  67. package/dist/assets/{selectionMarkdown-TkYk5h9l.js → selectionMarkdown-CUN3wUld.js} +9 -9
  68. package/dist/assets/{sessionOwnership-Bs1xbRfB.js → sessionOwnership-BSuxCEhb.js} +1 -1
  69. package/dist/assets/shikiHighlight-DHDx-6eM.js +1 -0
  70. package/dist/assets/space-apply-DM_2pRW7.js +1 -0
  71. package/dist/assets/space-close-Bpomt8z0.js +1 -0
  72. package/dist/assets/{surface-B6n7wJtt.js → surface-C-jQaGnL.js} +1 -1
  73. package/dist/assets/{textMateThemeFromAppTheme-CDlKffWz.js → textMateThemeFromAppTheme-DlsTphbY.js} +1 -1
  74. package/dist/assets/tr-gaTrX_HK.js +11 -0
  75. package/dist/assets/uk-CnJF6ykl.js +20 -0
  76. package/dist/assets/{useAppFontEffects-DCqCVGlT.js → useAppFontEffects-DGlvOqP_.js} +194 -233
  77. package/dist/assets/useInlineCommentController-kMN4Wg_D.js +2 -0
  78. package/dist/assets/{useRootScrollLock-BowwS8kq.js → useRootScrollLock-DMPpvpNP.js} +2 -2
  79. package/dist/assets/{vendor-base-ui-DX96QhTP.js → vendor-base-ui-Det7U81h.js} +1 -1
  80. package/dist/assets/{vendor-codemirror-autocomplete-DW1FQz9_.js → vendor-codemirror-autocomplete-BSHQHbKu.js} +1 -1
  81. package/dist/assets/{vendor-codemirror-commands-DpmhalwN.js → vendor-codemirror-commands-C4G_dPWe.js} +1 -1
  82. package/dist/assets/{vendor-codemirror-lang-angular-BqExbN1M.js → vendor-codemirror-lang-angular-B6LbBGHB.js} +1 -1
  83. package/dist/assets/{vendor-codemirror-lang-cpp-DZA36HfS.js → vendor-codemirror-lang-cpp-BWWXBx7W.js} +1 -1
  84. package/dist/assets/{vendor-codemirror-lang-css-B07blnnZ.js → vendor-codemirror-lang-css-Cayfb29q.js} +1 -1
  85. package/dist/assets/{vendor-codemirror-lang-go-BM8K9D1-.js → vendor-codemirror-lang-go-BoKlCNKw.js} +1 -1
  86. package/dist/assets/{vendor-codemirror-lang-html-DaH9adfE.js → vendor-codemirror-lang-html-BQWerHsJ.js} +1 -1
  87. package/dist/assets/{vendor-codemirror-lang-java-CsdC0viz.js → vendor-codemirror-lang-java-DWX7Fq_K.js} +1 -1
  88. package/dist/assets/{vendor-codemirror-lang-javascript-CpGYXGuo.js → vendor-codemirror-lang-javascript-DbpEcZM0.js} +2 -2
  89. package/dist/assets/{vendor-codemirror-lang-jinja-BBeGzXw5.js → vendor-codemirror-lang-jinja-D1JIJRA7.js} +1 -1
  90. package/dist/assets/{vendor-codemirror-lang-json-58T70Hqs.js → vendor-codemirror-lang-json-BzgckzmB.js} +1 -1
  91. package/dist/assets/{vendor-codemirror-lang-less-BI-xCA59.js → vendor-codemirror-lang-less-B9EpDXFi.js} +1 -1
  92. package/dist/assets/{vendor-codemirror-lang-liquid-zEcMa-zy.js → vendor-codemirror-lang-liquid-9U7kRbSf.js} +1 -1
  93. package/dist/assets/{vendor-codemirror-lang-markdown-BLKnlLP4.js → vendor-codemirror-lang-markdown-D2TEBIvZ.js} +1 -1
  94. package/dist/assets/{vendor-codemirror-lang-php-9f35M83-.js → vendor-codemirror-lang-php-BT6sDVzE.js} +1 -1
  95. package/dist/assets/vendor-codemirror-lang-python-jAMtYiA9.js +3 -0
  96. package/dist/assets/{vendor-codemirror-lang-rust-MNDL_jZQ.js → vendor-codemirror-lang-rust-CVO56EiL.js} +1 -1
  97. package/dist/assets/{vendor-codemirror-lang-sass-p9epsSNm.js → vendor-codemirror-lang-sass-DZlOoUWu.js} +1 -1
  98. package/dist/assets/{vendor-codemirror-lang-sql-Bqzh0EXX.js → vendor-codemirror-lang-sql-fZ6t1DPw.js} +1 -1
  99. package/dist/assets/{vendor-codemirror-lang-vue-BnNUWV-p.js → vendor-codemirror-lang-vue-DxbqrLiE.js} +1 -1
  100. package/dist/assets/{vendor-codemirror-lang-wast-C2a-oLmk.js → vendor-codemirror-lang-wast-DyU1Gt-I.js} +1 -1
  101. package/dist/assets/vendor-codemirror-lang-xml-C9a_K5_p.js +1 -0
  102. package/dist/assets/{vendor-codemirror-lang-yaml-C0sjswB0.js → vendor-codemirror-lang-yaml-Cptq8l0e.js} +1 -1
  103. package/dist/assets/{vendor-codemirror-language-ByBn5zIW.js → vendor-codemirror-language-DufXoQvE.js} +2 -2
  104. package/dist/assets/vendor-codemirror-language-data-CtwSWKtX.js +2 -0
  105. package/dist/assets/{vendor-codemirror-search-ib-sHD5W.js → vendor-codemirror-search-CvXM_pZg.js} +1 -1
  106. package/dist/assets/{vendor-codemirror-state-G3UuR-be.js → vendor-codemirror-state-Dek9sMtw.js} +1 -1
  107. package/dist/assets/{vendor-codemirror-view-Bniqp7p5.js → vendor-codemirror-view-1e0FQ6Wn.js} +1 -1
  108. package/dist/assets/vendor-framer-motion-DxDVhOLc.js +9 -0
  109. package/dist/assets/vendor-katex-CuQvj9-L.js +257 -0
  110. package/dist/assets/vendor-katex-DdZNUVNG.css +1 -0
  111. package/dist/assets/{vendor-motion-dom-DcCgeG69.js → vendor-motion-dom-HXPOMxb7.js} +1 -1
  112. package/dist/assets/{vendor-opencode-client-DHzhmLwV.js → vendor-opencode-client-BmFcUkbQ.js} +1 -1
  113. package/dist/assets/{vendor-replit-codemirror-vim-DkzqxN2H.js → vendor-replit-codemirror-vim-BlJRn-cW.js} +2 -2
  114. package/dist/assets/{vendor-tanstack-react-virtual-H_JmXOFV.js → vendor-tanstack-react-virtual-CvuLsk7D.js} +1 -1
  115. package/dist/assets/{vendor-tanstack-virtual-core-Dxc8hSTB.js → vendor-tanstack-virtual-core-DPaNcJcT.js} +1 -1
  116. package/dist/assets/zh-CN-t2vigSB1.js +20 -0
  117. package/dist/assets/zh-TW-jAgqBokB.js +20 -0
  118. package/dist/index.html +6 -6
  119. package/dist/mini-chat.html +6 -6
  120. package/dist/mobile.html +6 -6
  121. package/package.json +8 -5
  122. package/server/index.js +80 -36
  123. package/server/lib/agent-memory/actions.js +1 -6
  124. package/server/lib/agent-memory/routes.js +1 -2
  125. package/server/lib/agent-memory/runtime.js +1 -8
  126. package/server/lib/agent-tool/DOCUMENTATION.md +7 -3
  127. package/server/lib/agent-tool/runtime.js +3 -6
  128. package/server/lib/agent-tool/runtime.test.js +0 -2
  129. package/server/lib/browser-control/DOCUMENTATION.md +6 -0
  130. package/server/lib/changelog/update-notes.js +1 -1
  131. package/server/lib/context-obligatory/runtime.js +1 -2
  132. package/server/lib/dev-tunnel/DOCUMENTATION.md +8 -0
  133. package/server/lib/dev-tunnel/client.js +21 -10
  134. package/server/lib/dev-tunnel/tunnel.test.js +34 -0
  135. package/server/lib/dispatch-results/runtime.js +1 -1
  136. package/server/lib/enterprise-mode.d.ts +1 -0
  137. package/server/lib/enterprise-mode.js +13 -2
  138. package/server/lib/enterprise-mode.test.js +21 -0
  139. package/server/lib/event-stream/DOCUMENTATION.md +2 -2
  140. package/server/lib/event-stream/index.js +1 -3
  141. package/server/lib/event-stream/translate-v2.js +1 -9
  142. package/server/lib/event-stream/translate-v2.test.js +0 -11
  143. package/server/lib/fs/DOCUMENTATION.md +5 -2
  144. package/server/lib/fs/routes.js +62 -15
  145. package/server/lib/fs/routes.test.js +278 -48
  146. package/server/lib/fs/workspace-file-names.js +104 -0
  147. package/server/lib/fs/workspace-file-names.test.js +62 -0
  148. package/server/lib/git/DOCUMENTATION.md +17 -5
  149. package/server/lib/git/contributor-provenance-storage.js +9 -6
  150. package/server/lib/git/contributor-provenance-storage.test.js +29 -0
  151. package/server/lib/git/credential-broker.js +1 -1
  152. package/server/lib/git/credential-resolver.js +1 -4
  153. package/server/lib/git/credentials.js +1 -0
  154. package/server/lib/git/discovery-endpoint.js +1 -1
  155. package/server/lib/git/identity-provisioning.js +2 -1
  156. package/server/lib/git/identity-storage.js +3 -4
  157. package/server/lib/git/lfs-discovery.js +14 -14
  158. package/server/lib/git/lfs-discovery.test.js +16 -6
  159. package/server/lib/git/network-operation-plan.js +50 -62
  160. package/server/lib/git/network-operation-plan.test.js +103 -1
  161. package/server/lib/git/network-operation-registry.js +4 -6
  162. package/server/lib/git/network-operation-storage.js +4 -6
  163. package/server/lib/git/network-operations.js +12 -4
  164. package/server/lib/git/network-operations.test.js +19 -6
  165. package/server/lib/git/redaction.js +2 -1
  166. package/server/lib/git/repository-credential-runtime.js +5 -3
  167. package/server/lib/git/repository-credential-runtime.test.js +22 -0
  168. package/server/lib/git/routes.js +31 -137
  169. package/server/lib/git/routes.test.js +2 -2
  170. package/server/lib/git/service.js +143 -27
  171. package/server/lib/git/service.test.js +215 -1
  172. package/server/lib/git/ssh-credential-storage.js +1 -4
  173. package/server/lib/git/submodule-discovery.js +1 -1
  174. package/server/lib/git/worktree-bootstrap-storage.js +3 -4
  175. package/server/lib/git/worktree-bootstrap-storage.test.js +15 -0
  176. package/server/lib/git/worktree-creation.js +194 -0
  177. package/server/lib/git/worktree-creation.test.js +155 -0
  178. package/server/lib/github/DOCUMENTATION.md +11 -6
  179. package/server/lib/github/auth.js +2 -4
  180. package/server/lib/github/device-flow.js +2 -2
  181. package/server/lib/github/gh-cli-credential.js +1 -0
  182. package/server/lib/github/octokit.js +31 -0
  183. package/server/lib/github/pr-status.js +35 -18
  184. package/server/lib/github/pr-status.test.js +271 -3
  185. package/server/lib/github/reference-search.js +102 -13
  186. package/server/lib/github/reference-search.test.js +48 -7
  187. package/server/lib/github/routes.auth.test.js +18 -0
  188. package/server/lib/github/routes.js +360 -6
  189. package/server/lib/github/routes.mutations.test.js +239 -0
  190. package/server/lib/github/routes.test.js +134 -2
  191. package/server/lib/gitlab/DOCUMENTATION.md +5 -1
  192. package/server/lib/gitlab/auth-storage.test.js +1 -0
  193. package/server/lib/gitlab/mappers.js +30 -1
  194. package/server/lib/gitlab/resources.js +230 -6
  195. package/server/lib/gitlab/resources.test.js +179 -0
  196. package/server/lib/gitlab/routes.js +200 -6
  197. package/server/lib/gitlab/routes.test.js +186 -2
  198. package/server/lib/gitlab/validation.js +1 -5
  199. package/server/lib/guests/DOCUMENTATION.md +1 -1
  200. package/server/lib/guests/oauth.js +2 -2
  201. package/server/lib/guests/service.js +1 -1
  202. package/server/lib/guests/sockets.js +1 -1
  203. package/server/lib/injected-env.d.ts +8 -0
  204. package/server/lib/injected-env.js +50 -0
  205. package/server/lib/injected-env.test.js +56 -0
  206. package/server/lib/linear/DOCUMENTATION.md +5 -6
  207. package/server/lib/linear/issues.js +62 -4
  208. package/server/lib/linear/issues.test.js +68 -0
  209. package/server/lib/linear/oauth.js +5 -5
  210. package/server/lib/linear/parse.js +2 -9
  211. package/server/lib/message-queue/DOCUMENTATION.md +23 -1
  212. package/server/lib/message-queue/runtime.js +85 -12
  213. package/server/lib/message-queue/runtime.test.js +172 -2
  214. package/server/lib/notifications/apns-runtime.js +5 -6
  215. package/server/lib/notifications/apns-runtime.test.js +20 -4
  216. package/server/lib/openchamber-control/DOCUMENTATION.md +19 -1
  217. package/server/lib/openchamber-control/actions.js +2 -1
  218. package/server/lib/openchamber-control/service.js +119 -20
  219. package/server/lib/openchamber-control/service.test.js +166 -0
  220. package/server/lib/openchamber-sessions/archive-store.js +1 -6
  221. package/server/lib/openchamber-sessions/routes.js +47 -12
  222. package/server/lib/openchamber-sessions/routes.test.js +65 -2
  223. package/server/lib/openchamber-sessions/session-metadata-store.js +1 -6
  224. package/server/lib/opencode/DOCUMENTATION.md +22 -8
  225. package/server/lib/opencode/agents.js +62 -0
  226. package/server/lib/opencode/auth-state-runtime.js +3 -1
  227. package/server/lib/opencode/auth.d.ts +1 -1
  228. package/server/lib/opencode/auth.js +8 -0
  229. package/server/lib/opencode/auth.test.js +31 -0
  230. package/server/lib/opencode/config-entity-routes.js +16 -0
  231. package/server/lib/opencode/config-entity-v2.test.js +16 -1
  232. package/server/lib/opencode/config-paths.test.js +2 -2
  233. package/server/lib/opencode/config-v2.js +2 -4
  234. package/server/lib/opencode/feature-routes-runtime.js +89 -49
  235. package/server/lib/opencode/lifecycle.js +41 -21
  236. package/server/lib/opencode/lifecycle.test.js +50 -2
  237. package/server/lib/opencode/model-discovery.js +1 -1
  238. package/server/lib/opencode/model-discovery.test.js +17 -0
  239. package/server/lib/opencode/models-metadata.js +0 -2
  240. package/server/lib/opencode/openchamber-routes.test.js +3 -3
  241. package/server/lib/opencode/plugins.js +6 -9
  242. package/server/lib/opencode/provider-source-route.test.js +101 -3
  243. package/server/lib/opencode/providers-disabled.test.js +59 -0
  244. package/server/lib/opencode/providers.js +34 -0
  245. package/server/lib/opencode/proxy.js +11 -3
  246. package/server/lib/opencode/routes.js +40 -2
  247. package/server/lib/opencode/settings-helpers.js +27 -1
  248. package/server/lib/opencode/settings-helpers.test.js +19 -7
  249. package/server/lib/opencode/settings-registry.json +27 -16
  250. package/server/lib/opencode/settings-runtime.test.js +18 -0
  251. package/server/lib/opencode/shared-service-runtime.js +23 -104
  252. package/server/lib/opencode/shared.js +77 -46
  253. package/server/lib/opencode/shared.test.js +12 -12
  254. package/server/lib/opencode/shutdown-runtime.test.js +2 -3
  255. package/server/lib/opencode/skill-routes.js +123 -38
  256. package/server/lib/opencode/skill-routes.test.js +201 -0
  257. package/server/lib/opencode/skills.js +81 -34
  258. package/server/lib/opencode/skills.test.js +176 -21
  259. package/server/lib/opencode/startup-pipeline-runtime.js +6 -3
  260. package/server/lib/opencode/startup-pipeline-runtime.test.js +2 -1
  261. package/server/lib/opencode/watcher.test.js +0 -54
  262. package/server/lib/outbound-proxy.js +52 -0
  263. package/server/lib/outbound-proxy.test.js +55 -0
  264. package/server/lib/package-manager.js +23 -17
  265. package/server/lib/package-manager.test.js +5 -18
  266. package/server/lib/permission-auto-accept/DOCUMENTATION.md +1 -1
  267. package/server/lib/permission-auto-accept/runtime.js +19 -47
  268. package/server/lib/permission-auto-accept/runtime.test.js +2 -2
  269. package/server/lib/project-context/DOCUMENTATION.md +7 -1
  270. package/server/lib/project-context/routes.js +1 -2
  271. package/server/lib/project-context/runtime.js +12 -14
  272. package/server/lib/project-context/runtime.test.js +78 -2
  273. package/server/lib/projects/project-config.js +30 -15
  274. package/server/lib/projects/project-config.test.js +30 -0
  275. package/server/lib/projects/project-setup.js +1 -2
  276. package/server/lib/projects/routes.js +1 -2
  277. package/server/lib/quota/DOCUMENTATION.md +35 -7
  278. package/server/lib/quota/index.js +1 -0
  279. package/server/lib/quota/providers/claude/index.js +1 -1
  280. package/server/lib/quota/providers/cline-pass.js +1 -1
  281. package/server/lib/quota/providers/exe-dev.js +1 -1
  282. package/server/lib/quota/providers/google/index.js +1 -1
  283. package/server/lib/quota/providers/hyper.js +1 -1
  284. package/server/lib/quota/providers/kimi.js +116 -36
  285. package/server/lib/quota/providers/kimi.test.js +128 -3
  286. package/server/lib/quota/providers/opencode-go.js +144 -21
  287. package/server/lib/quota/providers/opencode-go.test.js +229 -6
  288. package/server/lib/quota/providers/openrouter.js +1 -1
  289. package/server/lib/quota/providers/zenmux.js +1 -1
  290. package/server/lib/quota/utils/auth.js +1 -0
  291. package/server/lib/quota/utils/transformers.js +2 -6
  292. package/server/lib/relay/DOCUMENTATION.md +18 -3
  293. package/server/lib/relay/e2ee.js +7 -7
  294. package/server/lib/relay/host-client.js +84 -13
  295. package/server/lib/relay/host-client.test.js +201 -1
  296. package/server/lib/relay/identity.js +6 -29
  297. package/server/lib/relay/identity.test.js +18 -28
  298. package/server/lib/relay/key-store.js +141 -0
  299. package/server/lib/relay/key-store.test.js +138 -0
  300. package/server/lib/relay/service.js +8 -8
  301. package/server/lib/relay/service.test.js +10 -5
  302. package/server/lib/relay/signing-key.js +13 -36
  303. package/server/lib/relay/tunnel-codec.js +10 -10
  304. package/server/lib/route-module-exports.test.js +22 -0
  305. package/server/lib/routing/DOCUMENTATION.md +23 -3
  306. package/server/lib/routing/cache.js +87 -0
  307. package/server/lib/routing/defaults.js +11 -0
  308. package/server/lib/routing/history.js +12 -4
  309. package/server/lib/routing/runtime.js +39 -3
  310. package/server/lib/routing/runtime.test.js +81 -2
  311. package/server/lib/routing/store.js +12 -1
  312. package/server/lib/routing/store.test.js +13 -0
  313. package/server/lib/scheduled-tasks/DOCUMENTATION.md +59 -1
  314. package/server/lib/scheduled-tasks/existing-session.js +119 -0
  315. package/server/lib/scheduled-tasks/existing-session.test.js +350 -0
  316. package/server/lib/scheduled-tasks/loops.js +1 -8
  317. package/server/lib/scheduled-tasks/routes.js +8 -8
  318. package/server/lib/scheduled-tasks/runtime.js +103 -13
  319. package/server/lib/scheduled-tasks/runtime.test.js +48 -0
  320. package/server/lib/scheduled-tasks/service.js +33 -11
  321. package/server/lib/scheduled-tasks/service.test.js +128 -2
  322. package/server/lib/scheduled-tasks/session-defaults.js +27 -0
  323. package/server/lib/scheduled-tasks/session-defaults.test.js +19 -0
  324. package/server/lib/session-assist/context.js +1 -0
  325. package/server/lib/session-folders/routes.js +2 -2
  326. package/server/lib/session-goal/objectives.js +1 -1
  327. package/server/lib/session-knowledge/routes.js +2 -2
  328. package/server/lib/session-knowledge/runtime.js +1 -2
  329. package/server/lib/session-work/DOCUMENTATION.md +39 -14
  330. package/server/lib/session-work/questions.js +31 -2
  331. package/server/lib/session-work/runtime.js +22 -13
  332. package/server/lib/session-work/runtime.test.js +43 -26
  333. package/server/lib/session-work/state.js +10 -4
  334. package/server/lib/session-work/state.test.js +8 -1
  335. package/server/lib/shared/DOCUMENTATION.md +89 -0
  336. package/server/lib/shared/guards.js +53 -0
  337. package/server/lib/shared/guards.test.js +112 -0
  338. package/server/lib/skills-catalog/DOCUMENTATION.md +10 -8
  339. package/server/lib/skills-catalog/cache.js +3 -2
  340. package/server/lib/skills-catalog/curated-sources.js +5 -1
  341. package/server/lib/skills-catalog/git.js +107 -20
  342. package/server/lib/skills-catalog/git.test.js +78 -0
  343. package/server/lib/skills-catalog/install.js +21 -12
  344. package/server/lib/skills-catalog/scan.js +25 -15
  345. package/server/lib/skills-catalog/source.js +26 -1
  346. package/server/lib/skills-catalog/source.test.js +27 -0
  347. package/server/lib/source-control/DOCUMENTATION.md +2 -1
  348. package/server/lib/source-control/audit-storage.js +7 -6
  349. package/server/lib/source-control/audit-storage.test.js +11 -0
  350. package/server/lib/source-control/binding-service.js +2 -5
  351. package/server/lib/source-control/change-request-worktree.js +90 -0
  352. package/server/lib/source-control/change-request-worktree.test.js +121 -0
  353. package/server/lib/source-control/mutation-executor.js +1 -1
  354. package/server/lib/source-control/mutation-storage.js +11 -5
  355. package/server/lib/source-control/provider-instance.js +1 -2
  356. package/server/lib/source-control/repository-identity.js +1 -1
  357. package/server/lib/source-control/routes.js +14 -2
  358. package/server/lib/spaces/DOCUMENTATION.md +42 -17
  359. package/server/lib/spaces/command-stream.test.js +15 -0
  360. package/server/lib/spaces/dispatcher.js +7 -0
  361. package/server/lib/spaces/dispatcher.test.js +17 -0
  362. package/server/lib/spaces/hardening.js +2 -2
  363. package/server/lib/spaces/hardening.test.js +1 -1
  364. package/server/lib/spaces/host.js +2 -0
  365. package/server/lib/spaces/journey.js +66 -5
  366. package/server/lib/spaces/journey.test.js +95 -4
  367. package/server/lib/spaces/layout.js +1 -1
  368. package/server/lib/spaces/places/docker-disk.js +38 -14
  369. package/server/lib/spaces/places/docker-disk.test.js +80 -1
  370. package/server/lib/spaces/places/docker.js +35 -11
  371. package/server/lib/spaces/places/fake-docker.js +22 -3
  372. package/server/lib/spaces/places/memory-place.js +3 -0
  373. package/server/lib/spaces/places/packed.docker.live.test.js +1 -1
  374. package/server/lib/spaces/routes.js +5 -0
  375. package/server/lib/spaces/routes.test.js +4 -2
  376. package/server/lib/spaces/run-command.js +9 -2
  377. package/server/lib/spaces/tools.js +4 -5
  378. package/server/lib/spaces/tools.test.js +2 -2
  379. package/server/lib/terminal/runtime.js +18 -6
  380. package/server/lib/terminal/runtime.test.js +66 -0
  381. package/server/lib/tts/index.js +3 -0
  382. package/server/lib/tts/service.js +4 -2
  383. package/server/lib/walkthrough/DOCUMENTATION.md +3 -1
  384. package/server/lib/walkthrough/index.js +2 -3
  385. package/server/lib/walkthrough/pr-source.test.js +22 -0
  386. package/server/lib/walkthrough/sources.js +6 -1
  387. package/server/opencode-proxy.test.js +127 -0
  388. package/bin/lib/commands-startup.test.js +0 -15
  389. package/dist/assets/CodeMirrorCommentWidgets-CVDSjcrP.js +0 -1
  390. package/dist/assets/DiffView-Ct7vL3fD.js +0 -4
  391. package/dist/assets/FilesView-DT0uEYgT.js +0 -17
  392. package/dist/assets/GuestSurfacePane-DVvk1VPu.js +0 -1
  393. package/dist/assets/LinearIssuesView-C10MwA9h.js +0 -1
  394. package/dist/assets/MarkdownImageGallery-BbN_K_3d.js +0 -1
  395. package/dist/assets/MarkdownRendererImpl-BLt2Tkk9.js +0 -10
  396. package/dist/assets/OnboardingScreen-Bgp5A3WB.js +0 -1
  397. package/dist/assets/PierreDiffViewer-CWJLj-S-.js +0 -210
  398. package/dist/assets/PlanView-DsRWnsOI.js +0 -5
  399. package/dist/assets/PublishDialog-CCeKloRu.js +0 -2
  400. package/dist/assets/SettingsView-BuPPsNPp.js +0 -30
  401. package/dist/assets/SettingsWindow-CM5fL8Ot.js +0 -1
  402. package/dist/assets/SubmoduleDiffSummary-CfsMcfiQ.js +0 -1
  403. package/dist/assets/ToolOutputDialog-mZrfSofz.js +0 -15
  404. package/dist/assets/ToolPartDiffPreview-BUGCRYEL.js +0 -8
  405. package/dist/assets/WalkthroughView-ClBo7PPd.js +0 -4
  406. package/dist/assets/appThemeRegistry-CxzMxR-H.js +0 -1
  407. package/dist/assets/de-CBla4hjd.js +0 -14
  408. package/dist/assets/es-BmnZtxTD.js +0 -20
  409. package/dist/assets/fileTreeStatus-BSHFIGEg.js +0 -1
  410. package/dist/assets/fr-OVKIBUZO.js +0 -18
  411. package/dist/assets/index-CKCFKmjF.js +0 -1
  412. package/dist/assets/index-Dbx4V4GN.css +0 -1
  413. package/dist/assets/index-vQ1Pg1cr.js +0 -61
  414. package/dist/assets/ja-6v-yr-gE.js +0 -20
  415. package/dist/assets/ko-I_ZLvgQ9.js +0 -20
  416. package/dist/assets/main-CmO4dRtR.js +0 -2
  417. package/dist/assets/main-b9vqhlYM.js +0 -999
  418. package/dist/assets/markdownCore-BPEZRQfP.js +0 -7
  419. package/dist/assets/miniChat-B5Oi7B-6.js +0 -2
  420. package/dist/assets/mobile-CVtShif9.js +0 -2
  421. package/dist/assets/nl-DcvIJf46.js +0 -20
  422. package/dist/assets/patchFileDiff-CW3wCH1O.js +0 -13
  423. package/dist/assets/pl-JxCUl0sd.js +0 -20
  424. package/dist/assets/pt-BR-L89mjja2.js +0 -20
  425. package/dist/assets/relocateRestoredSession-DYh_9oBa.js +0 -1
  426. package/dist/assets/renderElectronMiniChatApp-BD-1JHSg.js +0 -2
  427. package/dist/assets/renderMobileApp-7s5InXt1.js +0 -25
  428. package/dist/assets/runtimeEndpointReset-DqLJC6L1.js +0 -53
  429. package/dist/assets/shikiHighlight-CXgZnvHh.js +0 -1
  430. package/dist/assets/space-apply-1trGVIl8.js +0 -1
  431. package/dist/assets/space-close-y7j-NuBv.js +0 -1
  432. package/dist/assets/tr-ka5J1flt.js +0 -11
  433. package/dist/assets/uk-PyGe6Q_u.js +0 -20
  434. package/dist/assets/useInlineCommentController-CrjSGCpi.js +0 -2
  435. package/dist/assets/vendor-codemirror-lang-python-B7ClrMIT.js +0 -3
  436. package/dist/assets/vendor-codemirror-lang-xml-C8tGnS64.js +0 -1
  437. package/dist/assets/vendor-codemirror-language-data-q2bKO1EM.js +0 -2
  438. package/dist/assets/vendor-framer-motion-Bq3iTZ6r.js +0 -1
  439. package/dist/assets/vendor-katex-CHD9BdDO.js +0 -257
  440. package/dist/assets/vendor-katex-Cp579UoJ.css +0 -1
  441. package/dist/assets/zh-CN-B0P2tybJ.js +0 -20
  442. package/dist/assets/zh-TW-Bs9C5cN8.js +0 -20
package/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # <picture><source media="(prefers-color-scheme: dark)" srcset="https://github.com/openchamber/openchamber/raw/HEAD/docs/references/badges/openchamber-logo-dark.svg"><img src="https://github.com/openchamber/openchamber/raw/HEAD/docs/references/badges/openchamber-logo-light.svg" width="32" height="32" align="absmiddle" /></picture> @openchamber/web
1
+ # <picture><source media="(prefers-color-scheme: dark)" srcset="https://github.com/vantran-se/openchamber/raw/HEAD/docs/references/badges/openchamber-logo-dark.svg"><img src="https://github.com/vantran-se/openchamber/raw/HEAD/docs/references/badges/openchamber-logo-light.svg" width="32" height="32" align="absmiddle" /></picture> @vantran-se/openchamber-web
2
2
 
3
3
  [![GitHub stars](https://img.shields.io/github/stars/openchamber/openchamber?style=flat&logo=data%3Aimage%2Fsvg%2Bxml%3Bbase64%2CPHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIzMiIgaGVpZ2h0PSIzMiIgZmlsbD0iI2YxZWNlYyIgdmlld0JveD0iMCAwIDI1NiAyNTYiPjxwYXRoIGQ9Ik0yMjkuMDYsMTA4Ljc5bC00OC43LDQyLDE0Ljg4LDYyLjc5YTguNCw4LjQsMCwwLDEtMTIuNTIsOS4xN0wxMjgsMTg5LjA5LDczLjI4LDIyMi43NGE4LjQsOC40LDAsMCwxLTEyLjUyLTkuMTdsMTQuODgtNjIuNzktNDguNy00MkE4LjQ2LDguNDYsMCwwLDEsMzEuNzMsOTRMOTUuNjQsODguOGwyNC42Mi01OS42YTguMzYsOC4zNiwwLDAsMSwxNS40OCwwbDI0LjYyLDU5LjZMMjI0LjI3LDk0QTguNDYsOC40NiwwLDAsMSwyMjkuMDYsMTA4Ljc5WiIgb3BhY2l0eT0iMC4yIj48L3BhdGg%2BPHBhdGggZD0iTTIzOS4xOCw5Ny4yNkExNi4zOCwxNi4zOCwwLDAsMCwyMjQuOTIsODZsLTU5LTQuNzZMMTQzLjE0LDI2LjE1YTE2LjM2LDE2LjM2LDAsMCwwLTMwLjI3LDBMOTAuMTEsODEuMjMsMzEuMDgsODZhMTYuNDYsMTYuNDYsMCwwLDAtOS4zNywyOC44Nmw0NSwzOC44M0w1MywyMTEuNzVhMTYuMzgsMTYuMzgsMCwwLDAsMjQuNSwxNy44MkwxMjgsMTk4LjQ5bDUwLjUzLDMxLjA4QTE2LjQsMTYuNCwwLDAsMCwyMDMsMjExLjc1bC0xMy43Ni01OC4wNyw0NS0zOC44M0ExNi40MywxNi40MywwLDAsMCwyMzkuMTgsOTcuMjZabS0xNS4zNCw1LjQ3LTQ4LjcsNDJhOCw4LDAsMCwwLTIuNTYsNy45MWwxNC44OCw2Mi44YS4zNy4zNywwLDAsMS0uMTcuNDhjLS4xOC4xNC0uMjMuMTEtLjM4LDBsLTU0LjcyLTMzLjY1YTgsOCwwLDAsMC04LjM4LDBMNjkuMDksMjE1Ljk0Yy0uMTUuMDktLjE5LjEyLS4zOCwwYS4zNy4zNywwLDAsMS0uMTctLjQ4bDE0Ljg4LTYyLjhhOCw4LDAsMCwwLTIuNTYtNy45MWwtNDguNy00MmMtLjEyLS4xLS4yMy0uMTktLjEzLS41cy4xOC0uMjcuMzMtLjI5bDYzLjkyLTUuMTZBOCw4LDAsMCwwLDEwMyw5MS44NmwyNC42Mi01OS42MWMuMDgtLjE3LjExLS4yNS4zNS0uMjVzLjI3LjA4LjM1LjI1TDE1Myw5MS44NmE4LDgsMCwwLDAsNi43NSw0LjkybDYzLjkyLDUuMTZjLjE1LDAsLjI0LDAsLjMzLjI5UzIyNCwxMDIuNjMsMjIzLjg0LDEwMi43M1oiPjwvcGF0aD48L3N2Zz4%3D&logoColor=FFFCF0&labelColor=100F0F&color=66800B)](https://github.com/openchamber/openchamber/stargazers)
4
4
  [![GitHub release](https://img.shields.io/github/v/release/openchamber/openchamber?style=flat&logo=data%3Aimage%2Fsvg%2Bxml%3Bbase64%2CPHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIzMiIgaGVpZ2h0PSIzMiIgZmlsbD0iI2YxZWNlYyIgdmlld0JveD0iMCAwIDI1NiAyNTYiPjxwYXRoIGQ9Ik0xMjgsMTI5LjA5VjIzMmE4LDgsMCwwLDEtMy44NC0xbC04OC00OC4xOGE4LDgsMCwwLDEtNC4xNi03VjgwLjE4YTgsOCwwLDAsMSwuNy0zLjI1WiIgb3BhY2l0eT0iMC4yIj48L3BhdGg%2BPHBhdGggZD0iTTIyMy42OCw2Ni4xNSwxMzUuNjgsMThhMTUuODgsMTUuODgsMCwwLDAtMTUuMzYsMGwtODgsNDguMTdhMTYsMTYsMCwwLDAtOC4zMiwxNHY5NS42NGExNiwxNiwwLDAsMCw4LjMyLDE0bDg4LDQ4LjE3YTE1Ljg4LDE1Ljg4LDAsMCwwLDE1LjM2LDBsODgtNDguMTdhMTYsMTYsMCwwLDAsOC4zMi0xNFY4MC4xOEExNiwxNiwwLDAsMCwyMjMuNjgsNjYuMTVaTTEyOCwzMmw4MC4zNCw0NC0yOS43NywxNi4zLTgwLjM1LTQ0Wk0xMjgsMTIwLDQ3LjY2LDc2bDMzLjktMTguNTYsODAuMzQsNDRaTTQwLDkwbDgwLDQzLjc4djg1Ljc5TDQwLDE3NS44MlptMTc2LDg1Ljc4aDBsLTgwLDQzLjc5VjEzMy44MmwzMi0xNy41MVYxNTJhOCw4LDAsMCwwLDE2LDBWMTA3LjU1TDIxNiw5MHY4NS43N1oiPjwvcGF0aD48L3N2Zz4%3D&logoColor=FFFCF0&labelColor=100F0F&color=205EA6)](https://github.com/openchamber/openchamber/releases/latest)
@@ -11,23 +11,12 @@ Full project overview, screenshots, and all features: [github.com/openchamber/op
11
11
  ## Install
12
12
 
13
13
  ```bash
14
- curl -fsSL https://raw.githubusercontent.com/openchamber/openchamber/main/scripts/install.sh | bash
14
+ curl -fsSL https://raw.githubusercontent.com/vantran-se/openchamber/main/scripts/install.sh | bash
15
15
  ```
16
16
 
17
- Or install manually: `bun add -g @openchamber/web` (or npm, pnpm, yarn).
17
+ Or install manually: `bun add -g @vantran-se/openchamber-web` (or npm, pnpm, yarn).
18
18
 
19
- > **Prerequisites:** [OpenCode CLI](https://opencode.ai) installed, Node.js 22+.
20
-
21
- Start OpenCode first, then OpenChamber in another terminal:
22
-
23
- ```bash
24
- opencode
25
- openchamber
26
- ```
27
-
28
- Both clients use the same local OpenCode service, sessions, and live events. Normal local use does not require `opencode serve`, an OpenCode port, or skip-start configuration. Stopping OpenChamber Web leaves OpenCode, the TUI, and current work running.
29
-
30
- Shared Web does not install OpenChamber managed tools. Those tools remain available in Electron Desktop's managed OpenCode child.
19
+ > **Prerequisites:** [OpenCode CLI](https://opencode.ai) installed, Node.js 24.14+.
31
20
 
32
21
  ## Usage
33
22
 
@@ -52,14 +41,20 @@ openchamber connect-url --port 3000 # Add this server to OpenChamber Desktop
52
41
  openchamber connect-url --server http://host:3000 --qr
53
42
  openchamber connect-url --port 3000 --qr
54
43
  openchamber logs # Follow latest instance logs
55
- OPENCODE_HOST=https://myhost:4096 openchamber # Connect to an external OpenCode server
56
- openchamber stop # Stop Web; OpenCode and the TUI keep running
44
+ OPENCODE_PORT=4096 OPENCODE_SKIP_START=true openchamber # Connect to external OpenCode server
45
+ OPENCODE_HOST=https://myhost:4096 OPENCODE_SKIP_START=true openchamber # Connect via custom host/HTTPS
46
+ openchamber stop # Stop server
57
47
  openchamber update # Update to latest version
58
48
  ```
59
49
 
60
50
  `startup enable` snapshots your current environment into the native service so startup behaves like you launched `openchamber` from the same shell. This preserves provider tokens, PATH, SSH agent settings, and other CLI auth/config env vars. Use `--no-env-snapshot` for a minimal service env.
61
51
 
62
- Electron Desktop keeps its managed OpenCode child and environment-backed OpenChamber tools. Ordinary Web does not install or advertise those tools. An external endpoint selected with `OPENCODE_HOST` may have user-configured tools, but OpenChamber does not install or guarantee them.
52
+ When OpenChamber launches the local OpenCode server, it also registers a native
53
+ `openchamber` agent tool for project, session, and scheduled-task orchestration.
54
+ The tool is not injected when connecting to an external OpenCode server.
55
+ Behavior settings can optionally inject a managed system-prompt optimizer on
56
+ the next OpenCode restart. It is disabled by default and is not available for
57
+ external OpenCode servers.
63
58
 
64
59
  ### Tunnel behavior notes
65
60
 
@@ -106,25 +101,37 @@ Generating a client token does not automatically password-protect the hosted bro
106
101
  <summary>Connect to external OpenCode server</summary>
107
102
 
108
103
  ```bash
109
- OPENCODE_HOST=https://myhost:4096 openchamber
104
+ OPENCODE_PORT=4096 OPENCODE_SKIP_START=true openchamber
105
+ OPENCODE_HOST=https://myhost:4096 OPENCODE_SKIP_START=true openchamber
110
106
  ```
111
107
 
112
- `OPENCODE_HOST` overrides shared local discovery. Web treats that endpoint as externally owned: it connects API and event streams but never starts, stops, or restarts the endpoint. OpenChamber managed tools are not guaranteed there.
113
-
114
108
  | Variable | Description |
115
109
  |----------|-------------|
116
- | `OPENCODE_HOST` | Full base URL of an externally owned OpenCode server |
117
- | `OPENCHAMBER_HOST` | Bind hostname for the OpenChamber web server (default: `127.0.0.1`; use `0.0.0.0` for LAN/remote access on trusted networks) |
110
+ | `OPENCODE_HOST` | Full base URL of external server (overrides `OPENCODE_PORT`) |
111
+ | `OPENCODE_PORT` | Port of external server |
112
+ | `OPENCODE_SKIP_START` | Skip starting embedded OpenCode server |
113
+ | `OPENCHAMBER_OPENCODE_HOSTNAME` | Bind hostname for managed OpenCode server (default: `127.0.0.1`, use `0.0.0.0` for LAN/remote access — trusted networks only). Invalid values are rejected with an error and fall back to loopback |
114
+ | `OPENCHAMBER_HOST` | Bind hostname for the OpenChamber web server (default: `127.0.0.1`; use `0.0.0.0` for LAN/remote access — trusted networks only) |
118
115
  | `OPENCHAMBER_VERBOSE_REQUEST_LOGS` | Set to `true` to log every HTTP request; disabled by default to keep user logs small |
119
116
  | `OPENCHAMBER_GITLAB_CLIENT_ID` | OAuth application ID used for GitLab device flow. Overrides the built-in application, which covers `gitlab.com` only; a self-managed instance needs its own. Personal access token and `glab` authentication remain available without it. |
120
117
  | `OPENCHAMBER_SKIP_API_COMPRESSION` | Set to `true` to disable gzip compression for `/api/*` responses |
121
- | `OPENCHAMBER_COMPRESS_API` | Set to `true` to force `/api/*` compression, or `false` to disable it. Desktop disables API compression by default to reduce local sidecar CPU use |
118
+ | `OPENCHAMBER_COMPRESS_API` | Set to `true` to force `/api/*` compression, or `false` to disable it. Desktop runtime disables API compression by default to reduce local sidecar CPU use |
122
119
  | `OPENCHAMBER_FS_UPLOAD_MAX_BYTES` | Maximum file upload size in bytes (default: 100 MiB) |
123
120
  | `OPENCHAMBER_TERMINAL_SHELL` | Preferred terminal shell executable used by the `Auto` setting before platform defaults |
124
121
  | `npm_config_registry` / `NPM_CONFIG_REGISTRY` | Registry used for plugin metadata and npm version checks. Scoped registry settings and the user's `.npmrc` are also honored. The lowercase value takes precedence when both are set. |
125
122
 
126
123
  </details>
127
124
 
125
+ <details>
126
+ <summary>Bind managed OpenCode to LAN / Tailscale</summary>
127
+
128
+ ```bash
129
+ OPENCHAMBER_OPENCODE_HOSTNAME=0.0.0.0 openchamber --port 3000
130
+ ```
131
+
132
+ **Security note:** binding to `0.0.0.0` exposes the server on all network interfaces — use only on trusted networks and protect with firewall rules or `--ui-password`.
133
+
134
+ </details>
128
135
 
129
136
  **Optional env vars:**
130
137
  ```yaml
@@ -209,6 +216,7 @@ After=opencode.service
209
216
  Type=simple
210
217
  ExecStart=openchamber serve --port 3000 --host 0.0.0.0 --ui-password your-password --foreground
211
218
  Environment="OPENCODE_HOST=http://localhost:4095"
219
+ Environment="OPENCODE_SKIP_START=true"
212
220
  Restart=on-failure
213
221
  RestartSec=5
214
222
 
package/bin/cli.js CHANGED
@@ -414,9 +414,7 @@ if (isCliExecution) {
414
414
 
415
415
  main().catch((error) => {
416
416
  const message = error instanceof Error ? error.message : String(error);
417
- if (error?.cliReported === true) {
418
- process.exit(EXIT_CODE.GENERAL_ERROR);
419
- } else if (isJsonMode(activeCommandOptions)) {
417
+ if (isJsonMode(activeCommandOptions)) {
420
418
  printJson({
421
419
  status: 'error',
422
420
  error: {
package/bin/cli.test.js CHANGED
@@ -8,11 +8,12 @@ import { spawn } from 'child_process';
8
8
  import { pathToFileURL } from 'url';
9
9
 
10
10
  import { isModuleCliExecution, normalizeCliEntryPath } from './cli-entry.js';
11
+ import { INJECTED_ENV_KEY, assignInjectedEnv } from '../server/lib/injected-env.js';
11
12
  import { requestJson } from './lib/cli-http.js';
12
13
  import { requestControlAction } from './lib/cli-control.js';
13
14
  import { inspectTunnelAttachability } from './lib/cli-lifecycle.js';
14
15
  import { startupCommand } from './lib/commands-startup.js';
15
- import { formatGoal } from './lib/commands-schedule.js';
16
+ import { formatGoal, scheduleCommand } from './lib/commands-schedule.js';
16
17
  import {
17
18
  buildSessionCreatePayload,
18
19
  buildSessionPromptPayload,
@@ -70,10 +71,10 @@ function createMockJsonResponse(body, ok = true) {
70
71
  };
71
72
  }
72
73
 
73
- async function captureStream(stream, fn) {
74
- const originalWrite = stream.write;
74
+ async function captureStdout(fn) {
75
+ const originalWrite = process.stdout.write;
75
76
  let output = '';
76
- stream.write = (chunk, encoding, callback) => {
77
+ process.stdout.write = (chunk, encoding, callback) => {
77
78
  output += Buffer.isBuffer(chunk) ? chunk.toString('utf8') : String(chunk);
78
79
  if (typeof encoding === 'function') encoding();
79
80
  if (typeof callback === 'function') callback();
@@ -83,18 +84,10 @@ async function captureStream(stream, fn) {
83
84
  await fn();
84
85
  return output;
85
86
  } finally {
86
- stream.write = originalWrite;
87
+ process.stdout.write = originalWrite;
87
88
  }
88
89
  }
89
90
 
90
- async function captureStdout(fn) {
91
- return captureStream(process.stdout, fn);
92
- }
93
-
94
- async function captureStderr(fn) {
95
- return captureStream(process.stderr, fn);
96
- }
97
-
98
91
  async function startMockOpenChamberServer(options = {}) {
99
92
  const runtime = options.runtime || 'web';
100
93
  const pid = Number.isFinite(options.pid) ? options.pid : null;
@@ -695,7 +688,6 @@ describe('cli API target resolution', () => {
695
688
  });
696
689
  });
697
690
 
698
-
699
691
  describe('network-exposed auth validation', () => {
700
692
  it('allows loopback without a UI password', () => {
701
693
  expect(() => assertAuthenticatedNetworkExposure({ host: '127.0.0.1' })).not.toThrow();
@@ -779,6 +771,22 @@ describe('serve host resolution', () => {
779
771
  }
780
772
  });
781
773
 
774
+ it('ignores an OPENCHAMBER_HOST that OpenChamber put into the shell', () => {
775
+ const previous = { OPENCHAMBER_HOST: process.env.OPENCHAMBER_HOST, [INJECTED_ENV_KEY]: process.env[INJECTED_ENV_KEY] };
776
+ delete process.env[INJECTED_ENV_KEY];
777
+ delete process.env.OPENCHAMBER_HOST;
778
+ assignInjectedEnv(process.env, { OPENCHAMBER_HOST: '0.0.0.0' });
779
+ try {
780
+ expect(resolveServeHost(undefined)).toBe('127.0.0.1');
781
+ expect(resolveServeHost('192.0.2.21')).toBe('192.0.2.21');
782
+ } finally {
783
+ for (const [key, value] of Object.entries(previous)) {
784
+ if (value === undefined) delete process.env[key];
785
+ else process.env[key] = value;
786
+ }
787
+ }
788
+ });
789
+
782
790
  it('prefers explicit --host over OPENCHAMBER_HOST', () => {
783
791
  const previous = process.env.OPENCHAMBER_HOST;
784
792
  process.env.OPENCHAMBER_HOST = '192.0.2.20';
@@ -862,6 +870,22 @@ describe('CLI HTTP helpers', () => {
862
870
  }
863
871
  });
864
872
 
873
+ it('reports the enabled state the server saved and fails when it did not change', async () => {
874
+ const originalFetch = globalThis.fetch;
875
+ let saved = { task: { id: 'loop:user:mine', name: 'mine', enabled: true }, enabled: true };
876
+ globalThis.fetch = async () => createMockJsonResponse(saved);
877
+ const options = { port: 45680, explicitPort: true, directory: '/repo', task: 'loop:user:mine', json: true };
878
+ try {
879
+ const output = await captureStdout(() => scheduleCommand(options, 'enable'));
880
+ expect(JSON.parse(output)).toMatchObject(saved);
881
+
882
+ saved = { task: { id: 'loop:user:mine', name: 'mine', enabled: false }, enabled: false };
883
+ await expect(scheduleCommand(options, 'enable')).rejects.toThrow('mine is still disabled.');
884
+ } finally {
885
+ globalThis.fetch = originalFetch;
886
+ }
887
+ });
888
+
865
889
  it.each(['oc_ui_session', 'oc_ui_session_3000'])('retries UI-authenticated API requests with the %s cookie', async (cookieName) => {
866
890
  await withTempOpenChamberDataDir(async () => {
867
891
  const port = 45678;
@@ -1030,8 +1054,8 @@ describe('cli entry detection', () => {
1030
1054
 
1031
1055
  describe('isOpenchamberCmdline', () => {
1032
1056
  it('accepts OpenChamber CLI and daemon cmdlines', () => {
1033
- expect(isOpenchamberCmdline('node /x/@openchamber/web/bin/cli.js serve')).toBe(true);
1034
- expect(isOpenchamberCmdline('node /x/@openchamber/web/server/index.js --port 9090')).toBe(true);
1057
+ expect(isOpenchamberCmdline('node /x/@vantran-se/openchamber-web/bin/cli.js serve')).toBe(true);
1058
+ expect(isOpenchamberCmdline('node /x/@vantran-se/openchamber-web/server/index.js --port 9090')).toBe(true);
1035
1059
  expect(isOpenchamberCmdline('bun /home/u/projects/openchamber/packages/web/server/index.js --port 3001')).toBe(true);
1036
1060
  });
1037
1061
 
@@ -15,12 +15,15 @@ Command modules implement user-facing commands and preserve output contracts acr
15
15
 
16
16
  - `commands-serve.js`
17
17
  - Implements `openchamber serve`.
18
- - Owns OpenCode CLI checks, port resolution, log rotation, PID/instance registry writes, foreground/background server launch, startup summaries, and foreground shutdown behavior.
18
+ - Owns OpenCode CLI checks, port resolution, log rotation, PID and instance registry writes, foreground or background server launch, startup summaries, and foreground shutdown behavior.
19
+ - Ordinary Web connects to OpenCode's shared local service. Startup failures use the existing output adapter so JSON remains JSON-only, quiet output stays concise, and non-TTY mode never prompts.
19
20
 
20
21
  - `commands-lifecycle.js`
21
22
  - Implements `openchamber stop` and `openchamber restart`.
22
23
  - Owns lifecycle stop/restart semantics, desktop-managed port rejection, unmanaged instance shutdown attempts, PID/instance cleanup, and restart reuse of stored instance options.
23
24
 
25
+ Stopping Web shuts down the selected OpenChamber instance but leaves a `shared-local` or `explicit-external` OpenCode service running. OpenCode process teardown belongs only to the Desktop `managed-owned` runtime.
26
+
24
27
  - `commands-status.js`
25
28
  - Implements `openchamber status`.
26
29
  - Formats discovered instances and tunnel readiness/status for human, quiet, and JSON output.
@@ -54,12 +57,11 @@ Command modules implement user-facing commands and preserve output contracts acr
54
57
  - Finds or starts a local instance and prints the browser/connect URL according to the selected output mode.
55
58
  - Emits a **pairing v2** link (`openchamber://connect?v=2&p=<base64url>`): it creates a one-time pairing session in the shared store (`client-pairing-sessions.json`) and encodes the pairing id + secret + transport candidates. The client redeems the secret over whichever candidate connects first (`/api/client-auth/pairing/redeem`). No standalone token is embedded — the QR itself is the single-use credential.
56
59
  - The default form advertises the resolved server URL as a direct (lan/tunnel) candidate and folds in a relay candidate when the host relay is enabled, so one link works on-LAN and off-network.
57
- - `--relay` builds a relay-only pairing link (the sole candidate is the relay transport), for sharing with a device that is not on the host's network — no server URL, no auto-start. The relay endpoint follows the administrator's pin (`pinnedRelayUrl`: policy file, then `OPENCHAMBER_RELAY_URL`) / the stored setting / the default, matching the running host; the host must be running with the relay enabled to serve the redeem over the tunnel.
60
+ - `--relay` builds a relay-only pairing link (the sole candidate is the relay transport), for sharing with a device that is not on the host's network — no server URL, no auto-start. The relay endpoint follows `OPENCHAMBER_RELAY_URL` / the stored setting / the default, matching the running host; the host must be running with the relay enabled to serve the redeem over the tunnel.
58
61
 
59
62
  - `commands-update.js`
60
63
  - Implements `openchamber update`.
61
64
  - Loads the package-manager helper, performs update flow, and coordinates restart behavior after updates.
62
- - Installs the exact version returned by the update check and verifies the globally installed version after the package manager exits; a zero exit status without the target version is a loud failure, not a success report (#3083).
63
65
 
64
66
  - `commands-tunnel.js`
65
67
  - Implements `openchamber tunnel` and its subcommands: `profile`, `providers`, `ready`, `doctor`, `status`, `start`, `stop`, and `completion`.
@@ -72,7 +74,6 @@ These modules hold reusable, non-presentational logic for commands.
72
74
 
73
75
  - `cli-args.js`
74
76
  - Argument parsing, defaults, help text, completion script generation, and typo suggestions.
75
- - `COMMAND_OWNED_FLAGS` lists flags only one family of commands reads (schedule, session, tunnel, logs). Such a flag on any other command is an `Unknown option for <command>` error in every output mode instead of being ignored; global and shared flags are not listed and stay accepted everywhere. Add a new command-specific flag there.
76
77
 
77
78
  - `cli-errors.js`
78
79
  - CLI exit codes and typed tunnel CLI errors.
@@ -126,7 +127,7 @@ These modules hold reusable, non-presentational logic for commands.
126
127
 
127
128
  - `cli-startup.js`
128
129
  - Native startup service detection, install/uninstall/status helpers, and platform-specific startup command execution.
129
- - The service runs the CLI by its resolved path. A pnpm global install resolves into a versioned `.pnpm` store directory that an update leaves behind, so the entrypoint is mapped back to the stable `node_modules/@openchamber/web` link when it exists.
130
+ - Installed startup services remain owned by their platform service manager. `startup start|stop|restart` controls that manager directly. Top-level lifecycle commands route the service's configured port through the same manager, and `update` restarts it after package installation instead of starting a duplicate daemon.
130
131
 
131
132
  - `cli-tunnel-profiles.js`
132
133
  - Tunnel profile normalization, token resolution/redaction, profile storage, migration, file-permission warnings, and managed-remote pair persistence.
@@ -1,4 +1,5 @@
1
1
  import { TunnelCliError, EXIT_CODE } from './cli-errors.js';
2
+ import { injectedEnvKeys } from '../../server/lib/injected-env.js';
2
3
 
3
4
  const DEFAULT_PORT = 3000;
4
5
  const DEFAULT_TAIL_LINES = 200;
@@ -83,7 +84,11 @@ function parseArgs(argv = process.argv.slice(2)) {
83
84
  const options = {
84
85
  port: DEFAULT_PORT,
85
86
  host: undefined,
86
- uiPassword: process.env.OPENCHAMBER_UI_PASSWORD || undefined,
87
+ // A password OpenChamber itself put into this shell (the desktop app's, a
88
+ // running server's) is not a default for a new one.
89
+ uiPassword: injectedEnvKeys(process.env).has('OPENCHAMBER_UI_PASSWORD')
90
+ ? undefined
91
+ : process.env.OPENCHAMBER_UI_PASSWORD || undefined,
87
92
  json: false,
88
93
  all: false,
89
94
  follow: true,
@@ -0,0 +1,34 @@
1
+ import { afterEach, describe, expect, it } from 'vitest';
2
+
3
+ import { parseArgs } from './cli-args.js';
4
+ import { INJECTED_ENV_KEY, assignInjectedEnv } from '../../server/lib/injected-env.js';
5
+
6
+ describe('parseArgs ui password default', () => {
7
+ const previous = {
8
+ OPENCHAMBER_UI_PASSWORD: process.env.OPENCHAMBER_UI_PASSWORD,
9
+ [INJECTED_ENV_KEY]: process.env[INJECTED_ENV_KEY],
10
+ };
11
+
12
+ afterEach(() => {
13
+ for (const [key, value] of Object.entries(previous)) {
14
+ if (value === undefined) delete process.env[key];
15
+ else process.env[key] = value;
16
+ }
17
+ });
18
+
19
+ it('takes a password the user exported', () => {
20
+ delete process.env[INJECTED_ENV_KEY];
21
+ process.env.OPENCHAMBER_UI_PASSWORD = 'theirs';
22
+
23
+ expect(parseArgs(['serve']).options.uiPassword).toBe('theirs');
24
+ });
25
+
26
+ it('ignores a password OpenChamber put into the shell', () => {
27
+ delete process.env[INJECTED_ENV_KEY];
28
+ delete process.env.OPENCHAMBER_UI_PASSWORD;
29
+ assignInjectedEnv(process.env, { OPENCHAMBER_UI_PASSWORD: 'the-desktop-apps' });
30
+
31
+ expect(parseArgs(['serve']).options.uiPassword).toBeUndefined();
32
+ expect(parseArgs(['serve', '--ui-password', 'chosen']).options.uiPassword).toBe('chosen');
33
+ });
34
+ });
@@ -234,7 +234,6 @@ async function discoverRunningInstances(options = {}) {
234
234
  mtime,
235
235
  startedAt,
236
236
  launchMode,
237
- startupService: storedOptions?.startupService === true,
238
237
  runtime: liveInfo.runtime,
239
238
  source: 'registry+probe',
240
239
  host: typeof confirmedHost === 'string' && confirmedHost.length > 0
@@ -7,6 +7,7 @@ import {
7
7
  isNetworkExposedBindHost,
8
8
  isUnsafeUnauthenticatedLanAllowed,
9
9
  } from '../../server/lib/security/bind-host.js';
10
+ import { injectedEnvKeys } from '../../server/lib/injected-env.js';
10
11
 
11
12
  // Browser-unsafe ports (Fetch/Chromium restricted ports).
12
13
  const UNSAFE_BROWSER_PORTS = new Set([
@@ -24,10 +25,15 @@ function isUnsafeBrowserPort(port) {
24
25
  }
25
26
 
26
27
  function resolveConfiguredBindHost(hostOverride) {
28
+ // A host OpenChamber itself put into this shell (the desktop app's, a
29
+ // running server's) is not a default for a new one.
30
+ const configuredHost = injectedEnvKeys(process.env).has('OPENCHAMBER_HOST')
31
+ ? undefined
32
+ : process.env.OPENCHAMBER_HOST;
27
33
  const configured = typeof hostOverride === 'string' && hostOverride.trim()
28
34
  ? hostOverride.trim()
29
- : typeof process.env.OPENCHAMBER_HOST === 'string'
30
- ? process.env.OPENCHAMBER_HOST.trim()
35
+ : typeof configuredHost === 'string'
36
+ ? configuredHost.trim()
31
37
  : '';
32
38
  return configured || '127.0.0.1';
33
39
  }
@@ -129,7 +129,7 @@ function isOpenchamberCmdline(cmdline) {
129
129
  return false;
130
130
  }
131
131
  // Every install path contains the "openchamber" segment — the npm package
132
- // (@openchamber/web) and the source checkout both do, for the foreground
132
+ // (@vantran-se/openchamber-web) and the source checkout both do, for the foreground
133
133
  // (bin/cli.js) and daemon (server/index.js) entrypoints alike. Matching the
134
134
  // path segment (not a generic "cli.js") keeps a recycled stranger such as
135
135
  // "npm-cli.js" or "agentmemory" from being mistaken for us.
@@ -6,6 +6,8 @@ import crypto from 'crypto';
6
6
 
7
7
  import { createSettingsAccessors } from './cli-settings-accessors.js';
8
8
  import { createRelayIdentityRuntime } from '../../server/lib/relay/identity.js';
9
+ import { createRelayKeyStore } from '../../server/lib/relay/key-store.js';
10
+ import { deriveServerId } from '../../server/lib/relay/signing-key.js';
9
11
 
10
12
  const withTempDir = async (fn) => {
11
13
  const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'oc-settings-accessors-'));
@@ -164,26 +166,32 @@ describe('cli settings accessors', () => {
164
166
  });
165
167
  });
166
168
 
167
- it('does not regenerate the relay identity off a corrupt settings file', async () => {
169
+ it('moves a legacy relay identity out of settings.json without changing the serverId', async () => {
168
170
  await withTempDir(async (dir) => {
169
171
  const accessors = makeAccessors(dir);
172
+ const relayKeyStore = createRelayKeyStore({ fsPromises: fs.promises, path, dataDir: dir, ...accessors });
173
+ const pair = crypto.generateKeyPairSync('ec', { namedCurve: 'P-256' });
174
+ const publicJwk = pair.publicKey.export({ format: 'jwk' });
170
175
  fs.writeFileSync(
171
176
  path.join(dir, 'settings.json'),
172
- JSON.stringify({
173
- relaySigningKey: {
174
- privateJwk: crypto.generateKeyPairSync('ec', { namedCurve: 'P-256' }).privateKey.export({ format: 'jwk' }),
175
- publicJwk: crypto.generateKeyPairSync('ec', { namedCurve: 'P-256' }).publicKey.export({ format: 'jwk' }),
176
- },
177
- }),
177
+ JSON.stringify({ relaySigningKey: { privateJwk: pair.privateKey.export({ format: 'jwk' }), publicJwk } }),
178
178
  );
179
- const identity = await createRelayIdentityRuntime({ crypto, ...accessors }).getRelayIdentity();
180
- const serverIdBefore = identity.serverId;
181
179
 
182
- // Corrupt the file, then ask for the identity again: the strict gate must
183
- // make this FAIL rather than mint a replacement keypair.
180
+ const identity = await createRelayIdentityRuntime({ crypto, relayKeyStore }).getRelayIdentity();
181
+
182
+ expect(identity.serverId).toBe(deriveServerId({ crypto }, publicJwk));
183
+ expect(JSON.parse(fs.readFileSync(path.join(dir, 'settings.json'), 'utf8')).relaySigningKey).toBeUndefined();
184
+ });
185
+ });
186
+
187
+ it('does not regenerate the relay identity off a corrupt settings file', async () => {
188
+ await withTempDir(async (dir) => {
189
+ const accessors = makeAccessors(dir);
190
+ const relayKeyStore = createRelayKeyStore({ fsPromises: fs.promises, path, dataDir: dir, ...accessors });
191
+ // The legacy key may sit in the unreadable file: fail rather than mint a replacement.
184
192
  fs.writeFileSync(path.join(dir, 'settings.json'), '{"relaySigningKey": {"unfinished');
185
- await expect(createRelayIdentityRuntime({ crypto, ...accessors }).getRelayIdentity()).rejects.toThrow();
186
- expect(serverIdBefore).toBeTruthy();
193
+ await expect(createRelayIdentityRuntime({ crypto, relayKeyStore }).getRelayIdentity()).rejects.toThrow();
194
+ expect(fs.existsSync(path.join(dir, 'relay-identity.json'))).toBe(false);
187
195
  });
188
196
  });
189
197
  });
@@ -7,6 +7,7 @@ import { EXIT_CODE, TunnelCliError } from './cli-errors.js';
7
7
  import { getDataDir } from './cli-paths.js';
8
8
  import { hasUiPasswordConfigured } from './cli-network.js';
9
9
  import { searchPathFor } from './cli-executables.js';
10
+ import { injectedEnvKeys } from '../../server/lib/injected-env.js';
10
11
 
11
12
  const STARTUP_SERVICE_ID = 'dev.openchamber.web';
12
13
 
@@ -79,9 +80,12 @@ function getWindowsStartupWrapperPath() {
79
80
  }
80
81
 
81
82
  function collectStartupEnv(options = {}) {
83
+ // Variables OpenChamber itself put into this shell (the desktop app's
84
+ // terminal, an agent's shell) describe that instance, not the service.
85
+ const injected = injectedEnvKeys(process.env);
82
86
  const env = options.envSnapshot === false ? {} : Object.fromEntries(
83
87
  Object.entries(process.env)
84
- .filter(([key, value]) => shouldPersistStartupEnv(key, value))
88
+ .filter(([key, value]) => !injected.has(key) && shouldPersistStartupEnv(key, value))
85
89
  .map(([key, value]) => [key, String(value)])
86
90
  );
87
91
 
@@ -162,7 +166,10 @@ function removeStartupEnvFile() {
162
166
  }
163
167
 
164
168
  const PNPM_STORE_SEGMENT = `${path.sep}.pnpm${path.sep}`;
165
- const PACKAGE_SEGMENT = `${path.sep}node_modules${path.sep}@openchamber${path.sep}web${path.sep}`;
169
+ const PACKAGE_SEGMENTS = [
170
+ `${path.sep}node_modules${path.sep}@vantran-se${path.sep}openchamber-web${path.sep}`,
171
+ `${path.sep}node_modules${path.sep}@openchamber${path.sep}web${path.sep}`,
172
+ ];
166
173
 
167
174
  // pnpm keeps every installed version in its own store directory and links the
168
175
  // current one into node_modules. A startup service pointed at the store path
@@ -170,13 +177,15 @@ const PACKAGE_SEGMENT = `${path.sep}node_modules${path.sep}@openchamber${path.se
170
177
  function stablePnpmEntrypoint(resolved, exists = fs.existsSync) {
171
178
  const storeIndex = resolved.indexOf(PNPM_STORE_SEGMENT);
172
179
  if (storeIndex < 0) return null;
173
- const packageIndex = resolved.indexOf(PACKAGE_SEGMENT, storeIndex);
174
- if (packageIndex < 0) return null;
180
+ const packageMatch = PACKAGE_SEGMENTS
181
+ .map((segment) => ({ segment, index: resolved.indexOf(segment, storeIndex) }))
182
+ .find(({ index }) => index >= 0);
183
+ if (!packageMatch) return null;
175
184
  const stable = path.join(
176
185
  resolved.slice(0, storeIndex),
177
- '@openchamber',
178
- 'web',
179
- resolved.slice(packageIndex + PACKAGE_SEGMENT.length),
186
+ packageMatch.segment.includes('@vantran-se') ? '@vantran-se' : '@openchamber',
187
+ packageMatch.segment.includes('@vantran-se') ? 'openchamber-web' : 'web',
188
+ resolved.slice(packageMatch.index + packageMatch.segment.length),
180
189
  );
181
190
  return exists(stable) ? stable : null;
182
191
  }
@@ -253,9 +262,7 @@ function buildMacosLaunchAgent(options = {}) {
253
262
  <array>
254
263
  ${argXml}
255
264
  </array>
256
- ${envXml} <key>ProcessType</key>
257
- <string>Background</string>
258
- <key>RunAtLoad</key>
265
+ ${envXml} <key>RunAtLoad</key>
259
266
  <true/>
260
267
  <key>KeepAlive</key>
261
268
  <true/>
@@ -508,7 +515,6 @@ export {
508
515
  stablePnpmEntrypoint,
509
516
  buildSystemdUserService,
510
517
  getStartupStatus,
511
- startupServicePort,
512
518
  enableStartupService,
513
519
  disableStartupService,
514
520
  controlStartupService,
@@ -1,7 +1,10 @@
1
+ import fs from 'node:fs';
2
+ import os from 'node:os';
1
3
  import path from 'node:path';
2
- import { describe, expect, it } from 'vitest';
4
+ import { describe, expect, it, vi } from 'vitest';
3
5
 
4
- import { buildSystemdUserService, stablePnpmEntrypoint } from './cli-startup.js';
6
+ import { buildSystemdUserService, enableStartupService, stablePnpmEntrypoint } from './cli-startup.js';
7
+ import { INJECTED_ENV_KEY, assignInjectedEnv } from '../../server/lib/injected-env.js';
5
8
 
6
9
  const join = (...parts) => path.join(...parts);
7
10
 
@@ -31,3 +34,90 @@ describe('buildSystemdUserService', () => {
31
34
  expect(unit).toMatch(/^SuccessExitStatus=143$/m);
32
35
  });
33
36
  });
37
+
38
+ describe('macOS startup service', () => {
39
+ it('writes a launch agent without background process throttling', () => {
40
+ const home = fs.mkdtempSync(path.join(os.tmpdir(), 'openchamber-startup-'));
41
+ const platform = Object.getOwnPropertyDescriptor(process, 'platform');
42
+ const previousDataDir = process.env.OPENCHAMBER_DATA_DIR;
43
+ const plistPath = path.join(home, 'Library', 'LaunchAgents', 'dev.openchamber.web.plist');
44
+ const writeFileSync = fs.writeFileSync;
45
+ const stopBeforeLaunchctl = new Error('Stop before activating launchd');
46
+
47
+ try {
48
+ Object.defineProperty(process, 'platform', { configurable: true, value: 'darwin' });
49
+ process.env.OPENCHAMBER_DATA_DIR = path.join(home, '.config', 'openchamber');
50
+ vi.spyOn(os, 'homedir').mockReturnValue(home);
51
+ vi.spyOn(fs, 'writeFileSync').mockImplementation((file, ...args) => {
52
+ writeFileSync(file, ...args);
53
+ if (file === plistPath) throw stopBeforeLaunchctl;
54
+ });
55
+
56
+ expect(() => enableStartupService({ envSnapshot: false })).toThrow(stopBeforeLaunchctl);
57
+ const plist = fs.readFileSync(plistPath, 'utf8');
58
+ expect(plist).not.toContain('<key>ProcessType</key>');
59
+ expect(plist).toMatch(/<key>RunAtLoad<\/key>\s*<true\/>/);
60
+ expect(plist).toMatch(/<key>KeepAlive<\/key>\s*<true\/>/);
61
+ } finally {
62
+ vi.restoreAllMocks();
63
+ Object.defineProperty(process, 'platform', platform);
64
+ if (previousDataDir === undefined) delete process.env.OPENCHAMBER_DATA_DIR;
65
+ else process.env.OPENCHAMBER_DATA_DIR = previousDataDir;
66
+ fs.rmSync(home, { recursive: true, force: true });
67
+ }
68
+ });
69
+
70
+ it('leaves what OpenChamber put into the enabling shell out of the service environment', () => {
71
+ const home = fs.mkdtempSync(path.join(os.tmpdir(), 'openchamber-startup-'));
72
+ const platform = Object.getOwnPropertyDescriptor(process, 'platform');
73
+ const plistPath = path.join(home, 'Library', 'LaunchAgents', 'dev.openchamber.web.plist');
74
+ const writeFileSync = fs.writeFileSync;
75
+ const stopBeforeLaunchctl = new Error('Stop before activating launchd');
76
+ // The shell of a desktop app's terminal, or an agent's: the user's own
77
+ // exports next to what the app and the managed OpenCode set.
78
+ const ours = {
79
+ OPENCHAMBER_RUNTIME: 'desktop',
80
+ OPENCHAMBER_SKIP_API_COMPRESSION: 'true',
81
+ OPENCHAMBER_UI_PASSWORD: 'desktop-password',
82
+ OPENCODE_SERVER_PASSWORD: 'managed-password',
83
+ };
84
+ const theirs = {
85
+ OPENCODE_HOST: 'http://opencode.lan:4096',
86
+ MY_PROVIDER_TOKEN: 'token',
87
+ OPENCHAMBER_DATA_DIR: path.join(home, '.config', 'openchamber'),
88
+ };
89
+ const touched = [...Object.keys(ours), ...Object.keys(theirs), INJECTED_ENV_KEY];
90
+ const previous = Object.fromEntries(touched.map((key) => [key, process.env[key]]));
91
+
92
+ try {
93
+ Object.defineProperty(process, 'platform', { configurable: true, value: 'darwin' });
94
+ vi.spyOn(os, 'homedir').mockReturnValue(home);
95
+ for (const key of touched) delete process.env[key];
96
+ Object.assign(process.env, theirs);
97
+ assignInjectedEnv(process.env, ours);
98
+ vi.spyOn(fs, 'writeFileSync').mockImplementation((file, ...args) => {
99
+ writeFileSync(file, ...args);
100
+ if (file === plistPath) throw stopBeforeLaunchctl;
101
+ });
102
+
103
+ expect(() => enableStartupService({ port: 3000, host: '0.0.0.0', uiPassword: 'service-password' })).toThrow(stopBeforeLaunchctl);
104
+ const plist = fs.readFileSync(plistPath, 'utf8');
105
+ for (const key of ['OPENCHAMBER_RUNTIME', 'OPENCHAMBER_SKIP_API_COMPRESSION', 'OPENCODE_SERVER_PASSWORD', INJECTED_ENV_KEY]) {
106
+ expect(plist).not.toContain(`<key>${key}</key>`);
107
+ }
108
+ for (const key of Object.keys(theirs)) {
109
+ expect(plist).toContain(`<key>${key}</key>`);
110
+ }
111
+ expect(plist).toContain('<string>service-password</string>');
112
+ expect(plist).not.toContain('desktop-password');
113
+ } finally {
114
+ vi.restoreAllMocks();
115
+ Object.defineProperty(process, 'platform', platform);
116
+ for (const key of touched) {
117
+ if (previous[key] === undefined) delete process.env[key];
118
+ else process.env[key] = previous[key];
119
+ }
120
+ fs.rmSync(home, { recursive: true, force: true });
121
+ }
122
+ });
123
+ });
@@ -15,6 +15,7 @@ import { getInstanceFilePath, readInstanceOptions } from './cli-process.js';
15
15
  import { createRemoteClientAuthRuntime } from '../../server/lib/client-auth/remote-clients.js';
16
16
  import { createClientPairingRuntime } from '../../server/lib/client-auth/pairing.js';
17
17
  import { createRelayIdentityRuntime } from '../../server/lib/relay/identity.js';
18
+ import { createRelayKeyStore } from '../../server/lib/relay/key-store.js';
18
19
  import { DEFAULT_RELAY_URL, pinnedRelayUrl } from '../../server/lib/relay/service.js';
19
20
  import { bytesToBase64Url } from '../../server/lib/relay/e2ee.js';
20
21
  import { createSettingsAccessors as createSettingsAccessorsModule } from './cli-settings-accessors.js';
@@ -80,7 +81,13 @@ async function buildRelayPairingCandidate() {
80
81
  const accessors = createSettingsAccessors();
81
82
  const settings = await accessors.readSettingsFromDiskMigrated();
82
83
  const relayUrl = resolveRelayUrl(settings);
83
- const identityRuntime = createRelayIdentityRuntime({ crypto, ...accessors });
84
+ const relayKeyStore = createRelayKeyStore({
85
+ fsPromises: fs.promises,
86
+ path,
87
+ dataDir: getOpenChamberDataDir(),
88
+ ...accessors,
89
+ });
90
+ const identityRuntime = createRelayIdentityRuntime({ crypto, relayKeyStore });
84
91
  const identity = await identityRuntime.getRelayIdentity();
85
92
  return {
86
93
  enabled: settings?.privateRelay?.enabled === true,